别催更啦!手淘全链路性能优化下篇--容器极速之路
作者|手淘用户体验提升项目组
出品|阿里巴巴新零售淘系技术部
历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)我们重点介绍了手淘在性能优化中的一些实践和思路,主要集中在原生的代码的优化,这次,我们将继续分享在手淘容器化页面如 H5 及 Weex 相关的优化实践。
Weex 化的店铺性能优化
▐ 背景
店铺业务做为商家运营、营销的主阵地,是动态化方案激进的跟进者之一。2015年上线 Weapp 技术,就是在一码多端上的尝试。2017年,WeappPlus 的项目斐然崛起也和店铺有着千丝万缕的联系。
没看错,就是 Weex 的第一个项目名。之后 Weex 如一阵飓风般刮过集团各大业务,或整体、或单页、或卡片的应用场景层出不穷。店铺作为第一批上线 Weex 的业务,承担积极的推动作用,甚至一度把客户端给做没了,客户端仅保留了店铺路由的逻辑。在页面渲染和呈现上,全业务、全页面都由 Weex 来承载。
但是众所周知,Weex 技术在带来种种优点的同时,也有一个无法避免的短板。页面进入速度会变慢(相对于纯客户端页面而言),特别是店铺这种既需要复杂交互,同时还有三方 ISV 开放能力,单个页面的 js 超过 500K 的复杂业务场景。
在结构上,店铺的分为 框架
- 子页
两部分:
首页就是屏幕中央的主要业务区域,可以通过 tab 完成多个页面间的切换。
框架 和 子页,通过 embed 标签完成嵌套关系。
同时为了让用户获得更大的浏览区域,引入了 Nested-Scroll, BindingX 等能力实现嵌套滚动,底色渐变等能力。
最后店铺外投的 URL 形式众多,需要有能力支持各式各样的入口地址。
综上,整个进店流程从链路上说可以分为三个阶段:路由,框架,首页。
接下来,将从设计、分析、实现、上线等角度阐述整个优化工作的具体实现。
希望能够帮助读者在优化自身业务时,有更多的方法。
▐ 开工
★ 优化开始的第一件事是什么?
对,就是找把尺子。
先确定好衡量的环境 和 数据口径,做到手中有尺,心中不慌。
只有明确了标准,才能使工作的价值 更清晰、更准确、更容易 得被衡量。
特别当兴冲冲得找老板汇报已经完成目标,结果一测发现还差一大截时,更能体会这条 tips 的重要。
★ 第二件 备好工具
直接代码打性能日志
(最简单也是最好用)
有个小建议,同时输出时间戳 和 时间差。同时按一条主要的关键路径,把先后的日志按顺序串起来。SystemClock.uptimeMillis() 不推荐使用,请使用 System.currentTimeMillis() 。
这样有个好处,当有多个系统交互,比如 前端 客户端java 客户端c++ 就在一条时间基线上,串起来看清晰很多。
Systrace + TraceView + top + Charles
Systrace: 目标是跑满 cpu,这个在项目优化初期做一下,对整个流程有个宏观感觉。项目冲刺阶段做一次,重点突破。
TraceView: 可多看看,找耗时方法很有效。
top: 在遇到分析瓶颈的时候,不妨试试,往往能看出一些线索,比如 USER CPU 占用偏高,重点分析是否和多线程场景 和 检查 c++ 任务调度。
Charles:在定位到底是 网络慢 还是 处理慢 的问题上,往往一针见血。
所有工具都是手段,最重要的还是对 流程的理解 和 业务的熟悉。
adb shell input tap
input tap 这个简单的特性,往往能化腐朽为神奇,减轻偶现问题的修复验证工作。
▐ 技术选型
回忆下前面提到的店铺渲染三大阶段:整个优化工作都是围绕这三个阶段展开。这里进一步对三步进行细化,见下图。
★ 框架 - 子页 并行渲染
在上述店铺三阶段中,可以发现 店铺框架 和 店铺首页,作为两个独立 WXSDKInstance,在整体流程中耗时占比较大。很朴素地就考虑到能不能将这两步并行起来。
优化前由前端代码通过 embed 标签,直接就能确定了「子页」和「框架」嵌套关系 和 嵌套位置。流程相对简单。
在引入并行渲染的优化后,事情变复杂。由客户端并行承载两个页面的绘制工作,再选择合适时机,把两个页面组合起来。
从流程上来,「框架」和「子页」的 weex 渲染任务会并行进行。并且同时取消了「子页」的 loading,在用户交互感知上少了一个等待的过程。体感上提示很明显。
★ 数据请求托管
经过分析,传统方式下,一次店铺打开,共有五次数据请求。虽然前端可以利用 Promise 技术做并发,但是依然存在等待数据的状态。
针对上述五次请求,具体场景不同有不同的优化策略,其基本方向还是数据请求合并,并发请求,异步请求。
过程中的注意点:
1. 最优先考虑的还是数据合并,将多个数据请求合并成一个,最简单且效果最好。
2. 并行数据请求,复用的是 weex 实例创建的时间,越早请求,效果越好。
3. 异步请求的场景,远程数据获取 和 前端数据拉取 的先后关系不定,要注意数据缓存的设计。
4. 对于所有处理数据的weex 模块,所有方法都不需要主线程执行。
★ JS-Bundle 提前准备
总所周知,优化 JS-Bundle 的加载速度,是提升 weex 业务体验最有效和最直接的抓手。
针对绝大多数 weex 场景,通过手淘现有Cache 技术完成 JS-Bundle 的客户端侧存储,已经可以很好地支持业务。但对于店铺这个 pv 亿计体量的业务还略显不足。主要体现在:
到达率无法准确把控
更新时间较久
磁盘加载稍慢。
针对上述几点改进空间,并且结合业务特点定制了 JS-Bundle 端侧存储能力。主要特点:
1. 除首次纯新安装场景,其余情况总是优先返回本地缓存的 JS-Bundle。同时根据两次访问的时间差,判断是否要刷新本地缓存。且遵循 http 的 cache-control 策略。
2. 客户端侧底层存储使用统一存储,在手淘打开后的首次访问时,读数据很慢,甚至在高端机上比走网络还慢。所以选取合适的时机做预热。
3. 内存级缓存。内部使用 SoftReference 包裹数据体,防止 OOM。同时在 切后台 和 接到低内存警告 的时候,触发对象释放机制。
在引入 JS-Bundle 提前准备后,用户最直观的感受就是整页白屏 loading 等待的时间大幅缩短,甚至页面转场完成时,首页就已经渲染完毕,避免了用户视觉上的空窗期。
★ 上下游优化
再提几个比较容易忽略的优化点。
Weex Module 的调用
每次 module 调用虽然耗时很短,但是因为每次都要等待 WXJSBridge 线程空闲,并且获取到 cpu 时间片才能执行。所以常常会出现方法调用耗时 10ms,等待耗时 300ms 的现象。这点主要从前端优化。
一方面尽量减少 module 调用,特别是关系到主链路的渲染流程中的 module 调用。
另一方面,将不重要的 module 调用延后。例如店铺就将部分 ut 埋点逻辑,做了延后处理。
流程上看,通过减少 JS-bridge 的任务调用,减少线程切换时间,从而减少 weex 渲染 view 的上屏时间。最终减少用户可流畅交互时间。
图片预加载
针对图文较多的业务,在客户端获取到数据接口后,直接利用图片库的预请求能力,做图片的加载。
硬件加速
部分手机厂商向公司开发了 API,允许在特定场景下调升 CPU 频率,从而实现加速的效果。
最后也是最简单的一点
减少 JS-Bundle 的文本大小,是最直观的提效做法。
优化业务的前端代码,去掉过期的逻辑,去除无用判断,去掉冗余的依赖。
店铺业务大约减少了 80K+ 的 js,获得了大约 40ms- 的性能提升。
综上:所有优化点上线后。整个流程链路以下图流程经行。
通过店铺性能调试工具可以实时验证观察各阶段的渲染效果。
Web 前端性能优化实践
前端 Web 页面的性能给大多数人的印象可能还停留在几年前的加载白屏、滑动体验差等,但是到了现在这个时间节点,必须停下来重新思考下现有的业务发展、技术开发方式等,最终我们觉得有理由并且有能力从全链路的角度来看前端 Web 页面性能,最终突破这一刻板印象。
▐ 优化思路
在进行优化前,梳理了几个原则:
1.明确目标:整体页面端内首屏时间 1.2s,端外首屏时间 3s,低端机首屏时间小于 2s,可交互时间小于 2.5s,可流畅交互时间小于 3s。性能优化无止尽,定好目标才能有的放矢。
2.梳理端到端系统化每个环节:梳理宏观的每个环节,再具体分析到细节点。
3.数据驱动性能优化:线上收集各个环节的性能数据,便于分析。
4.投入产出比 ROI: 不要迷失在性能优化中,思考技术对于业务的价值。
★ 各环节耗时分布
这张表统计了从点击开始到渲染完成各个环节的耗时分布,各个最低数值代表理论可行值,最大值代表线上真实数据耗时,因此可以清楚的知道哪个环节现阶段是最差的,然后进行针对性的分析和优化。
最终会不断的调整这张表的数值分布,然后看哪个阶段还能继续优化,形成优化并数值调整 -> 线上验证 -> 再优化调整的正向循环。
▐ 渲染方案
目前基于线上数据验证以及线下对比,如果一个页面资源(包括文档 HTML、js 资源)都是走缓存的形式,整体页面的性能、体验会比较好,对文档、资源的缓存非常关键。因此整体的渲染方案会最大限度的利用缓存,设计如下:
1.袋鼠服务:面向前端的数据接口合并服务,模块直接取数据渲染,保证了页面渲染的一致性
2.统一渲染页
数据驱动页面渲染,所有页面的核心逻辑一致
保证模块渲染与页面的分离,确保统一渲染页被稳定缓存
3.页面渲染策略:区别首屏/非首屏渲染,保证首屏能快速渲染
4.模块缓存池:解决所有页面模块缓存问题
▐ 优化系列
基于性能优化的手段,目前我们可以主要分为以下三种:
★ 加载
对于一张页面的加载,我们要做到 4 个 1:
一个共享文档
一个首屏关键 js
一个首屏业务 combo 资源
一个全局共享缓存池
主要罗列了以下几个优化手段:
1.按需加载:某些特别大的 js 资源可以按需加载,比如安全脚本大小会有 500K+
2.分环境加载:由于目前端上 es6 环境支持的程度已经较好,因此可以在绝大多数场景去除 babel-polyfill 的依赖
3.资源缓存:端上 combo 资源按版本拆分缓存
资源缓存
1.端上共享缓存池,缓存 combo 后的资源到内存以及缓存解 combo 后的缓存到内存/磁盘,这样在二次访问时达到重复利用的目的。
2.优化资源传递消耗,内核之间只需 Stream 流形式传递,避免了类型如 byte -> String -> byteStream 之间的类型转化消耗。
优势:
1.无需推送 ZCache,共享单个级别的资源
2.性能好:缓存 20-100ms vs网络请求 200-300ms+
3.优于 httpcache,可以命中更多的 combo 组合
劣势:
1.会占用一定的内存来换取资源读取上的优势
★ 渲染
对于渲染,我们要做到首屏尽可能快的渲染完成,主要罗列了以下几个优化手段:
1.首屏/非首屏渲染,更细粒度的按行懒加载:对于一整张页面,我们希望能优先渲染首屏,可以尽快的呈现内容让用户看到。有时候首屏的内容也会非常长,还可以做到按行级别的控制懒加载渲染。
2.js bridge 通道优化:端内需要有较多的与 native api 交互,通信效率也至关重要。
js bridge 通道优化
核心:解决JS引擎与内核 Java 层同步 IPC 通信阻塞问题
优化前
使用 prompt 方法进行 jsbridge 通信
1.JS 引擎与内核 java 层同步IPC通信,阻塞 Render 线程,通信耗时:中/低端机100-200毫秒,导致延长渲染时间
2.引发内存泄漏问题
3.引起渲染引擎重排版,平均耗时 100ms 左右
优化后
使用 onConsoleMessage 方法进行 jsbridge 通信
1.JS 引擎与内核 java 层异步IPC通信,不阻塞 Render 线程
2.不引起渲染引擎重排版
★ 数据
数据 Prefetch & 时机提前
核心:解决数据/渲染并行执行,尽可能的减少数据请求时间
1.利用了容器的初始化时间,进行并行数据请求,从而节省数据的请求时间
2.进一步提前数据请求时间点到点击
最终的节省时间 = 容器路由时间(找哪个容器渲染) + 容器启动时间 + 容器初始化时间 + 页面框架渲染时间
中间过程优化
核心:解决子线程与 UI 线程的阻塞问题,以及中间环节没必要的类型转换
优化前
优化后
最后
从宏观到微观。性能优化之初一定要有宏观视角,对整个框架的运行情况,要有做到心里有数,对问题进行全面分析,然后再对瓶颈进行拆解,拆解后的子任务也不能孤立去看,一定要放在系统内,综合选择最优方法。
建立长期管控机制。性能优化成本也非常昂贵,性能优化在某种程度上,与其说是技术人员秀肌肉,不如说是还债,技术债(当然,真正有技术挑战在一些特定条件下做极致优化的情况不在此列)。从最初的技术方案设计,业务压力下充忙上线,线性的功能堆积,对现有架构设计的妥协等多种原因导致了性能问题的。如果在上线之初就能考虑到对性能的影响,好好设计方案,这时的成本是最低的。然而,一切依赖人的行为的机制都是不靠谱的,老虎也有打盹儿时。要减少运动式的做性能优化,需要建立一个依赖于客观数据长效的监控机制,这也是我们正在探索的方向。
路漫漫其修远兮,吾将上下而求索。
今日话题
还想了解什么内容?下方评论区留言~你来说,我安排!
推荐阅读